# Provides a method to setup a field on a model.
module Volt
  module FieldHelpers
    class InvalidFieldClass < RuntimeError; end

    NUMERIC_CAST = lambda do |convert_method, val|
      begin
        orig = val

        val = send(convert_method, val)

        if RUBY_PLATFORM == 'opal'
          # Opal has a bug in 0.7.2 that gives us back NaN without an
          # error sometimes.
          val = orig if val.nan?
        end
      rescue TypeError, ArgumentError => e
        # ignore, unmatched types will be caught below.
        val = orig
      end

      return val
    end

    FIELD_CASTS = {
      String        => :to_s.to_proc,
      Fixnum        => lambda {|val| NUMERIC_CAST[:Integer, val] },
      Numeric       => lambda {|val| NUMERIC_CAST[:Float, val] },
      Float         => lambda {|val| NUMERIC_CAST[:Float, val] },
      TrueClass     => nil,
      FalseClass    => nil,
      NilClass      => nil,
      Volt::Boolean => nil,
    }

    # If VoltTime has already been required by the time we load this class, we
    # add it to the field casts:
    if defined?(VoltTime)
      FIELD_CASTS[VoltTime] = nil
    end


    module ClassMethods
      # field lets you declare your fields instead of using the underscore syntax.
      # An optional class restriction can be passed in.
      def field(name, klasses = nil, options = {})
        if klasses
          klasses = [klasses].flatten

          unless klasses.any? {|kl| FIELD_CASTS.key?(kl) }
            klass_names = FIELD_CASTS.keys.map(&:to_s).join(', ')
            msg = "valid field types is currently limited to #{klass_names}, you passed: #{klasses.inspect}"
            fail FieldHelpers::InvalidFieldClass, msg
          end

          # Add NilClass as an allowed type unless allow_nil: false was passed.
          unless options[:allow_nil] == false
            klasses << NilClass
          end
        end

        self.fields_data ||= {}
        self.fields_data[name] = [klasses, options]

        if klasses
          # Add type validation, execpt for String, since anything can be cast to
          # a string.
          unless klasses.include?(String)
            validate name, type: klasses
          end
        end

        define_method(name) do
          get(name)
        end

        define_method(:"#{name}=") do |val|
          # Check if the value assigned matches the class restriction
          if klasses
            # Cast to the right type
            klasses.each do |kl|
              if (func = FIELD_CASTS[kl])
                # Cast on the first available caster
                val = func[val]
                break
              end
            end
          end

          set(name, val)
        end
      end
    end

    def self.included(base)
      base.class_attribute :fields_data
      base.send :extend, ClassMethods
    end
  end
end
